package planning.heuristic;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.jgap.FitnessFunction;
import org.jgap.Gene;
import org.jgap.IChromosome;

import planning.util.Summary;

import commons.cloud.Contract;
import commons.cloud.MachineType;
import commons.cloud.Provider;
import commons.cloud.User;
import commons.cloud.UtilityResultEntry;
import commons.config.Configuration;
import commons.sim.util.SaaSAppProperties;
import commons.sim.util.SimulatorProperties;
import commons.util.SimulationInfo;
import edu.uah.math.distributions.BinomialDistribution;

/**
 * Fitness function that evaluates a reservation plan utility according to a queue network.
 * @author David Candeia Medeiros Maia - davidcmm@lsd.ufcg.edu.br
 *
 */
public class PlanningFitnessFunction_Prov_Sim extends FitnessFunction{

	private static double LOAD_FACTOR = 1;//DPS resource consumption / workload demand. First DPS value: 5.5; DPS with 95 percentile: 3.2

	public static final int HOUR_IN_MILLIS = 3600000;

	private long SUMMARY_LENGTH_IN_SECONDS;
	private double OPTIMAL_UTILIZATION = 0.75;//Resource target utilization
	
	private final Provider[] cloudProviders;
	private final User[] cloudUsers;
	private final Map<User, List<Summary>> summaries;
	private final List<MachineType> types;
	private Map<MachineType, Provider> providersToUse;
	
	private double totalOnDemandConsumedHrs;
	private Map<MachineType, Integer> reservedHoursConsumedPerMachineType;
//	private Map<Integer, Integer> machinesToObtain;

	
	//Logging
//	private FileWriter writer;
//	StringBuffer log = new StringBuffer();
	
	/**
	 * Default constructor.
	 * @param summaries Workload statistics.
	 * @param cloudUsers SaaS clients.
	 * @param cloudProviders IaaS providers.
	 * @param types Machine types. 
	 */
	public PlanningFitnessFunction_Prov_Sim(Map<User, List<Summary>> summaries, User[] cloudUsers, Provider[] cloudProviders, List<MachineType> types) {
		this.summaries = summaries;
		this.cloudProviders = cloudProviders;
		this.cloudUsers = cloudUsers;
		this.types = types;
		this.totalOnDemandConsumedHrs = 0d;
		this.providersToUse = new HashMap<MachineType, Provider>();
		
		try{
			this.SUMMARY_LENGTH_IN_SECONDS = Configuration.getInstance().getLong(SimulatorProperties.PLANNING_INTERVAL_SIZE);
		}catch(Exception e){
			this.SUMMARY_LENGTH_IN_SECONDS = 60 * 60;
		}
		
//		this.machinesToObtain = new HashMap<Integer, Integer>();
		
		//Logging
//		try {
//			writer = new FileWriter(new File("profits"+cloudProviders[0].getName()+".dat"));
//		} catch (IOException e) {
//		}
	}
	
	/**
	 * Evaluates a reservation plan.
	 */
	@Override
	protected double evaluate(IChromosome arg0) {
//		this.log = new StringBuffer();
		
		this.totalOnDemandConsumedHrs = 0d;
		this.providersToUse = new HashMap<MachineType, Provider>();
		this.reservedHoursConsumedPerMachineType = new HashMap<MachineType, Integer>();
		
		//Since round-robin is used, the total arrival rate is splitted among reserved servers according to each server number of cores
		Map<MachineType, Integer> currentMachinesPerMachineType = new HashMap<MachineType, Integer>();

		Map<MachineType, Integer> debugTypes = new HashMap<MachineType, Integer>();
		
		int index = 0;
		double totalCores = 0;
		
//		System.err.print("@@@@ RESERVATION:\t");
		for(Gene gene : arg0.getGenes()){
			Integer numberOfMachinesReserved = (Integer)gene.getAllele();
			MachineType type = this.types.get(index);
			
//			currentCoresPerMachineType.put(type, (int)Math.round(1.0 * numberOfMachinesReserved * type.getTotalCapacity()));
			currentMachinesPerMachineType.put(type, (int)Math.round(1.0 * numberOfMachinesReserved));
			totalCores += Math.round(1.0 * numberOfMachinesReserved * type.getTotalCapacity());
			index++;
			
			debugTypes.put(type, numberOfMachinesReserved);
//			System.err.print(type.toString()+"\t"+numberOfMachinesReserved);
		}
//		System.err.println();
//		if(debugTypes.get(MachineType.M1_XLARGE) == 0 && debugTypes.get(MachineType.M1_LARGE) == 0 && debugTypes.get(MachineType.M1_SMALL) == 17){
//			System.out.println("Cheguei!");
//		} else if(debugTypes.get(MachineType.M1_XLARGE) == 0 && debugTypes.get(MachineType.M1_LARGE) == 0 && debugTypes.get(MachineType.M1_SMALL) == 13){
//			System.out.println("Cheguei!");
//		} else if (debugTypes.get(MachineType.M1_XLARGE) == 0 && debugTypes.get(MachineType.M1_LARGE) == 0 && debugTypes.get(MachineType.M1_SMALL) == 10){
//			System.out.println("Cheguei!");
//		} else if (debugTypes.get(MachineType.M1_XLARGE) == 1 && debugTypes.get(MachineType.M1_LARGE) == 0 && debugTypes.get(MachineType.M1_SMALL) == 2){
//			System.out.println("Cheguei!");
//		} else if (debugTypes.get(MachineType.M1_XLARGE) == 0 && debugTypes.get(MachineType.M1_LARGE) == 0 && debugTypes.get(MachineType.M1_SMALL) == 0){
//			System.out.println("Cheguei!");
//		}
		
		Map<MachineType, Double> totalRequestsFinishedInReservedMachines = new HashMap<MachineType, Double>();
		double totalRequestsFinishedInOnDemandMachines = 0d;
		double requestsLostDueToResponseTime = 0;
		double requestsLostDueToThroughput = 0;
		double requestsProcessedFromQueue = 0;
		double accumulatedRequestsMeanServiceTime = 0d;
		
		int currentSummaryInterval = 0;
		int totalNumberOfIntervals = calcNumberOfIntervals();
		long maxResponseTimeInMillis = Configuration.getInstance().getLong(SaaSAppProperties.APPLICATION_SLA_MAX_RESPONSE_TIME);
		
		double onDemandRisk = ( 275 * Configuration.getInstance().getDouble(SimulatorProperties.PLANNING_NORMAL_RISK) + 
				45 * Configuration.getInstance().getDouble(SimulatorProperties.PLANNING_TRANS_RISK) + 
				45 * Configuration.getInstance().getDouble(SimulatorProperties.PLANNING_PEAK_RISK) ) / 365.0;
		
		//Machine types used in planning from biggest type to the smallest one
		List<MachineType> planTypes = new ArrayList<MachineType>();
		planTypes.add(MachineType.M1_XLARGE);
		planTypes.add(MachineType.M1_LARGE);
		planTypes.add(MachineType.M1_SMALL);
		
		while(currentSummaryInterval < totalNumberOfIntervals){//for each hour in the simulation period
			
			double arrivalRate = aggregateArrivals(currentSummaryInterval);
			double meanServiceTimeInMillis = aggregateServiceDemand(currentSummaryInterval);
			accumulatedRequestsMeanServiceTime += meanServiceTimeInMillis;
			
			//Assuming a base computing power (e.g., EC2 unit for each core)
			Map<MachineType, Double> throughputPerMachineType = new HashMap<MachineType, Double>();
			double reservedThroughput = 0d;
			double missingThroughput = 0d;
			
			//System.err.println("\n>> DEBUG R:\t" + arrivalRate + "\t"+ (arrivalRate * SUMMARY_LENGTH_IN_SECONDS) * meanServiceTimeInMillis / (1000 * 3600));
			
			double arrivalRateToProcess = arrivalRate;
			for(MachineType type : planTypes){
				
				if(arrivalRateToProcess > 0){
					double maximumThroughput = 0d;
					double individualThroughput = (1 / (meanServiceTimeInMillis/1000)) * type.getTotalCapacity() * OPTIMAL_UTILIZATION;
					
					if(meanServiceTimeInMillis != 0){
						maximumThroughput = individualThroughput * currentMachinesPerMachineType.get(type);//Using all cores
					}else{
						maximumThroughput = Double.MAX_VALUE;
					}
					
					double currentToProcess = Math.min(arrivalRateToProcess, maximumThroughput); 
					
					if(currentToProcess > 0){
						throughputPerMachineType.put(type, currentToProcess);
						reservedThroughput += currentToProcess;
						
						//Logging amount of requests finished
						Double currentFinished = totalRequestsFinishedInReservedMachines.get(type);
						if(currentFinished == null){
							currentFinished = 0d;
						}
						currentFinished += Math.ceil(currentToProcess * SUMMARY_LENGTH_IN_SECONDS);
						totalRequestsFinishedInReservedMachines.put(type, currentFinished);
						
						//Updating hours consumed for current machine type
						Integer totalConsumed = reservedHoursConsumedPerMachineType.get(type);
						if(totalConsumed == null){
							totalConsumed = new Integer(0);
						}
						totalConsumed += (int)Math.ceil(currentToProcess / individualThroughput);
						reservedHoursConsumedPerMachineType.put(type, totalConsumed);
					}
					arrivalRateToProcess -= currentToProcess;
				}
			}
			
			missingThroughput = arrivalRateToProcess;
			
			//Calculating missing requests. This value is amortized by reserved queue size!
			double requestsMissed = missingThroughput * SUMMARY_LENGTH_IN_SECONDS;
			if(missingThroughput > 0 && reservedThroughput != 0){//Queue starts at this interval, so some requests are not really missed!
				double requestsToProcessFromQueue = (totalCores * maxResponseTimeInMillis / meanServiceTimeInMillis - reservedThroughput) 
						* (HOUR_IN_MILLIS/maxResponseTimeInMillis) * OPTIMAL_UTILIZATION;
				
				//Should only add requests removed if there are requests missed!
				if(requestsToProcessFromQueue > 0){
					requestsProcessedFromQueue += Math.min(requestsToProcessFromQueue, requestsMissed); 
				}

				requestsMissed = Math.max(requestsMissed - requestsToProcessFromQueue, 0);
			}
			
			//Evaluating on-demand resources consumption because of requests that could not be processed in reserved resources
			if(requestsMissed > 0){
				int onDemandQueueSize = (int) Math.max(Math.ceil( ((maxResponseTimeInMillis / meanServiceTimeInMillis) * 1 * OPTIMAL_UTILIZATION) ), 0);//small machines
				int requestedOnDemandMachines = (int) Math.ceil(requestsMissed / (onDemandQueueSize * HOUR_IN_MILLIS/maxResponseTimeInMillis));
				
				int machinesObtained = (int) (requestedOnDemandMachines * (1- onDemandRisk));
				totalOnDemandConsumedHrs += machinesObtained;
				
				//Removing requests that were really processed
				long requestsProcessed = machinesObtained * ((onDemandQueueSize * HOUR_IN_MILLIS/maxResponseTimeInMillis));
				totalRequestsFinishedInOnDemandMachines += Math.min(requestsMissed, requestsProcessed);
				
				requestsMissed = Math.max(0, requestsMissed - requestsProcessed);
			}
			
			requestsLostDueToThroughput += requestsMissed;
			
			//Estimated response time
			double totalNumberOfUsers = 0.0;
			if(arrivalRate != 0){
				totalNumberOfUsers = aggregateNumberOfUsers(currentSummaryInterval) * (reservedThroughput / arrivalRate);
			}
			double averageThinkTimeInSeconds = aggregateThinkTime(currentSummaryInterval);
			if(reservedThroughput != 0){
				double responseTimeInSeconds = totalNumberOfUsers / reservedThroughput - averageThinkTimeInSeconds;
				double responseTimeLoss = Math.max( (responseTimeInSeconds * 1000 - maxResponseTimeInMillis)/maxResponseTimeInMillis, 0 );
				requestsLostDueToResponseTime += calcResponseTimeLoss(responseTimeLoss, totalRequestsFinishedInReservedMachines);
			}
			
			currentSummaryInterval++;
		}
		
		requestsLostDueToThroughput = Math.max(0, requestsLostDueToThroughput);
		
		//Estimating utility
		double receipt = calcReceipt();
		
		//>>> Debugging
//		this.log = new StringBuffer();
//		this.log.append(receipt+"\t");
		
		double cost = calcCost(totalRequestsFinishedInReservedMachines, totalRequestsFinishedInOnDemandMachines, accumulatedRequestsMeanServiceTime / totalNumberOfIntervals, currentMachinesPerMachineType, requestsLostDueToResponseTime, requestsLostDueToThroughput, requestsProcessedFromQueue);
		double fitness = receipt - cost;
		
//		this.log.append("TC\t"+cost+"\tP\t"+fitness);
//		try {
//			writer.write(this.log.toString()+"\n");
//			writer.flush();
//		} catch (IOException e) {
//		}
		
		if(fitness < 1){
			return (1/Math.abs(fitness))+1;
		}
		
		return fitness;
		
	}
	
	/**
	 * This method distributes a certain arrival rate to existing reserved machine according to their processing power
	 * @param currentPowerPerMachineType
	 * @param totalPower
	 * @param arrivalRate
	 * @return
	 */
	protected Map<MachineType, Double> extractArrivalsPerMachineType(Map<MachineType, Integer> currentPowerPerMachineType,
			double totalPower, double arrivalRate) {
		Map<MachineType, Double> arrivalRatesPerMachineType = new HashMap<MachineType, Double>();
		for(MachineType type : currentPowerPerMachineType.keySet()){
			if(totalPower == 0){
				arrivalRatesPerMachineType.put(type, 0d);
			}else{
				arrivalRatesPerMachineType.put(type, (currentPowerPerMachineType.get(type) / totalPower) * arrivalRate);
			}
		}
		return arrivalRatesPerMachineType;
	}
	
	/**
	 * This method gets the total amount of requests finished at a certain interval and applies a certain loss according to a percent
	 * of requests that finish after the SLA.
	 * @param responseTimeLoss
	 * @param totalRequestsFinished
	 * @return
	 */
	protected double calcResponseTimeLoss(double responseTimeLoss, Map<MachineType, Double> totalRequestsFinished) {
		double totalFinished = 0d;
		for(Double value : totalRequestsFinished.values()){
			totalFinished += value;
		}
		
		return totalFinished * responseTimeLoss;
	}
	
	/**
	 * This method calculates the number of intervals to be evaluated
	 * @return
	 */
	protected int calcNumberOfIntervals() {
		for(List<Summary> data : this.summaries.values()){
			return data.size();
		}
		
		return 0;
	}
	
	/**
	 * This method calculates penalties to be paid by the SaaS provider since some requests were not processed or some requests were
	 * processed outside the SLA.
	 * @param responseTimeRequestsLost Amount of requests that were processed outside the SLA
	 * @param requestsThatCouldNotBeAttended Amount of requests that were not processed
	 * @param totalRequestsFinished Amount of requests that were succesfully processed
	 * @return The penalty to be paid.
	 */
	protected double calcPenalties(double responseTimeRequestsLost, double requestsThatCouldNotBeAttended, double totalRequestsFinished){
		
		double lossPerUser = (responseTimeRequestsLost + requestsThatCouldNotBeAttended) / (requestsThatCouldNotBeAttended + responseTimeRequestsLost + totalRequestsFinished);
		//lossPerUser = lossPerUser / this.cloudUsers.length;
		
		if(totalRequestsFinished == 0){
			lossPerUser = 1;
		}
		
		double penalty = 0d;
		for(User user : this.cloudUsers){
			penalty += user.calculatePenalty(lossPerUser);
		}
		return penalty;
	}

	/**
	 * This method calculates the cost of reserved and on-demand consumption 
	 * @param requestsFinishedPerMachineType
	 * @param meanServiceTimeInMillis
	 * @param currentAmountOfMachinesPerMachineType
	 * @param requestsLostDueToResponseTime
	 * @param requestsLostDueToThroughput
	 * @param requestsProcessedFromQueue
	 * @return
	 */
	 protected double calcCost(Map<MachineType, Double> requestsFinishedPerMachineType, double totalRequestsFinishedInOnDemandMachines, 
			double meanServiceTimeInMillis, Map<MachineType, Integer> currentAmountOfMachinesPerMachineType, 
			double requestsLostDueToResponseTime, double requestsLostDueToThroughput, double requestsProcessedFromQueue) {

		 //Verifying on-demand resources that can be used
		long onDemandResources = Math.round(cloudProviders[0].getOnDemandLimit());
		long planningPeriod = Configuration.getInstance().getLong(SimulatorProperties.PLANNING_PERIOD);
		
//		Provider provider = cloudProviders[0];
		double cost = 0;
		
		//Reserved Costs
		double totalRequestsFinished = 0;
		for(MachineType type : currentAmountOfMachinesPerMachineType.keySet()){
			double requestsFinished = (requestsFinishedPerMachineType.get(type) == null)?0:requestsFinishedPerMachineType.get(type);
			totalRequestsFinished += requestsFinished;
			double CPUHoursPerType = (this.reservedHoursConsumedPerMachineType.get(type) == null)?0:this.reservedHoursConsumedPerMachineType.get(type);
			
			CPUHoursPerType = Math.ceil(CPUHoursPerType * LOAD_FACTOR);
			int amountOfMachines = currentAmountOfMachinesPerMachineType.get(type);
			
			//>> Debugging
//			this.log.append(type+"\t"+amountOfMachines);
//			double currentCost = 0d;
			
//			if(provider.getIsHeavy() && currentAmountOfMachinesPerMachineType.get(type) > 0){
//				currentCost = provider.getReservationOneYearFee(type) * amountOfMachines +
//						(provider.getMonitoringCost() + provider.getReservedCpuCost(type)) * (amountOfMachines * planningPeriod * 24);
//				cost += currentCost;
//				this.log.append("\t"+ CPUHoursPerType + "\t" + (amountOfMachines * planningPeriod * 24) + "\t" + currentCost + "\t");
//			}else if(currentAmountOfMachinesPerMachineType.get(type) > 0){
//				currentCost = provider.getReservationOneYearFee(type) * amountOfMachines 
//						+ (provider.getMonitoringCost() + provider.getReservedCpuCost(type)) * CPUHoursPerType;
//				cost += currentCost;
//				this.log.append("\t"+ CPUHoursPerType + "\t" + CPUHoursPerType + "\t"+ currentCost + "\t");
//			} else {
//				this.log.append("\t"+ 0 + "\t" + 0 + "\t"+ 0 + "\t");
//			}
			
			double minCost = Double.MAX_VALUE;
			Provider chosenProvider = null;
			for(Provider provider : this.cloudProviders){
				double currentCost = 0d;
				if(provider.getIsHeavy() && currentAmountOfMachinesPerMachineType.get(type) > 0){
					currentCost = provider.getReservationOneYearFee(type) * amountOfMachines +
							(provider.getMonitoringCost() + provider.getReservedCpuCost(type)) * (amountOfMachines * planningPeriod * 24);
				}else{
					currentCost = provider.getReservationOneYearFee(type) * ( amountOfMachines ) 
							+ (provider.getMonitoringCost() + provider.getReservedCpuCost(type)) * CPUHoursPerType;
				}
				if(currentCost < minCost){
					minCost = currentCost;
					chosenProvider = provider;
				}
			}
			cost += minCost;
			
			//Saving selected provider for current machine type
			this.providersToUse.put(type, chosenProvider);
		}

		//On-demand costs
		double adjustedOnDemandCPUHours = Math.ceil(totalOnDemandConsumedHrs * LOAD_FACTOR);
		
		//Computing requests finished using on-demand machines
		totalRequestsFinished += totalRequestsFinishedInOnDemandMachines;

		long requestsThatCouldNotBeAttended = 0;
		if(adjustedOnDemandCPUHours > onDemandResources * planningPeriod * 24){//Demand is greater than what could be retrieved!
			double extraHoursNeeded = adjustedOnDemandCPUHours - (onDemandResources * planningPeriod * 24);
			
			if(extraHoursNeeded > 0){
				requestsThatCouldNotBeAttended = Math.round( (extraHoursNeeded * HOUR_IN_MILLIS / meanServiceTimeInMillis) );
				totalRequestsFinished -= requestsThatCouldNotBeAttended; 
			}
			
			adjustedOnDemandCPUHours = onDemandResources * planningPeriod * 24;
		}
		
//		double onDemandCost = (provider.getMonitoringCost() + provider.getOnDemandCpuCost(MachineType.M1_SMALL)) * adjustedOnDemandCPUHours;
//		cost += onDemandCost;
		requestsThatCouldNotBeAttended += requestsLostDueToThroughput;
		
		//Checking which provider gives the best price
		double minCost = Double.MAX_VALUE;
		Provider chosenProvider = null;
		for(Provider provider : this.cloudProviders){
			double currentCost = (provider.getMonitoringCost() + provider.getOnDemandCpuCost(MachineType.M1_SMALL)) * adjustedOnDemandCPUHours;
			if(currentCost < minCost){
				minCost = currentCost;
				chosenProvider = provider;
			}
		}
		//Calculating on-demand cost
		cost += minCost;
		
		//>>Debugging
//		this.log.append("ON\t" + adjustedOnDemandCPUHours+"\t"+onDemandCost+"\t");

		//Penalties
		double penalties = calcPenalties(requestsLostDueToResponseTime, requestsThatCouldNotBeAttended, totalRequestsFinished);
		return cost + penalties;
	}
	 
	/**
	 * Estimating receipt obtained by SaaS provider. The receipt comes from periodical payments of each SaaS client and from
	 * setup fees.
	 * @return
	 */
	protected double calcReceipt() {
		UtilityResultEntry resultEntry = new UtilityResultEntry(0, this.cloudUsers, this.cloudProviders);

		double oneTimeFees = 0d;
		for(Entry<User, List<Summary>> entry : this.summaries.entrySet()){
			Contract contract = entry.getKey().getContract();
			int index = 0;
			int counter = 0;
			double totalCPUHrs = 0;
			
			for(Summary summary : entry.getValue()){
				counter++;
				totalCPUHrs += summary.getTotalCpuHrs();
				if(counter == (SimulationInfo.daysInMonths[index]+1) * 24){//Calculate receipt for a complete month!
					contract.calculateReceipt(resultEntry, entry.getKey().getId(), (long)Math.ceil(totalCPUHrs * 60 * 60 * 1000), 0l, 0l, 0l);
					index++;
					totalCPUHrs = 0;
				}
			}
			
			oneTimeFees += contract.calculateOneTimeFees();//Setup fees
		}
		
		return resultEntry.getReceipt() + oneTimeFees;
	}

	protected double aggregateThinkTime(int currentSummaryInterval) {
		double thinkTime = 0d;
		int totalNumberOfValues = 0;
		
		for(Entry<User, List<Summary>> entry : this.summaries.entrySet()){
			thinkTime += entry.getValue().get(currentSummaryInterval).getUserThinkTimeInSeconds();
			totalNumberOfValues++;
		}
		
		if(totalNumberOfValues == 0){
			return thinkTime;
		}
		return thinkTime / totalNumberOfValues;
	}

	protected double aggregateNumberOfUsers(int currentSummaryInterval) {
		int totalNumberOfUsers = 0;
		
		for(Entry<User, List<Summary>> entry : this.summaries.entrySet()){
			totalNumberOfUsers += entry.getValue().get(currentSummaryInterval).getNumberOfUsers();
		}
		
		return totalNumberOfUsers;
	}

	protected double aggregateServiceDemand(int currentSummaryInterval) {
		double serviceTime = 0d;
		int totalNumberOfValues = 0;
		
		for(Entry<User, List<Summary>> entry : this.summaries.entrySet()){
			serviceTime += entry.getValue().get(currentSummaryInterval).getRequestServiceDemandInMillis();
			totalNumberOfValues++;
		}
		
		if(totalNumberOfValues == 0){
			return serviceTime;
		}
		
		return serviceTime / totalNumberOfValues;
	}

	protected double aggregateArrivals(int currentSummaryInterval) {
		double totalArrivalRate = 0;
		for(Entry<User, List<Summary>> entry : this.summaries.entrySet()){
			totalArrivalRate += entry.getValue().get(currentSummaryInterval).getArrivalRate();
		}
		
		return totalArrivalRate;
	}

	public void close() {
//		try {
//			this.writer.close();
//		} catch (IOException e) {
//		}
	}
	
	/**
	 * This method returns the provider at which specific machine types should be reserved
	 * @return
	 */
	public Map<MachineType, Provider> getProvidersToUse() {
		return providersToUse;
	}
}
